package org.infoset.xml.util;

import java.util.*;
import java.io.*;
import java.net.*;
import java.util.logging.*;
import org.infoset.xml.*;

public class XMLWriter {
   
   final static int ARRAY_INCREMENT = 20;
   
   Logger log = Logger.getLogger("org.infoset.xml.util");

   boolean isEvents;
   NamespaceScope [] scopes;
   int level;
   boolean serializationStarted;
   boolean preferDefault;
   boolean omitXMLDeclaration;
   String doctypeSystemId;
   String doctypePublicId;
   Serializer serializer;
   boolean skipAncestorNamespaceDecls;
   Map<URI,Object> excludedNamespaces;
   URI currentBaseURI;

   public XMLWriter(boolean isEvents,Serializer serializer) {
      this.isEvents = isEvents;
      this.scopes = new NamespaceScope[ARRAY_INCREMENT];
      scopes[0] = NamespaceScope.getDefaultScope();
      this.serializer = serializer;
      this.preferDefault = false;
      this.skipAncestorNamespaceDecls = false;
      this.excludedNamespaces = new HashMap<URI,Object>();
      this.omitXMLDeclaration = false;
      this.level = -1;
      this.serializationStarted = false;
      this.doctypeSystemId = null;
      this.doctypePublicId = null;
   }
   
   public void excludeNamespace(URI namespace) {
      excludedNamespaces.put(namespace, Boolean.TRUE);
      if (log.isLoggable(Level.FINE)) {
         log.fine("Excluding namespace: "+namespace);
      }
   }
   
   public void excludeNamespaces(Set<URI> namespaces) {
      for (URI ns : namespaces) {
         excludedNamespaces.put(ns,Boolean.TRUE);
         if (log.isLoggable(Level.FINE)) {
            log.fine("Excluding namespace: "+ns);
         }
      }
   }
   
   public void setDoctypePublicId(String value)
   {
      doctypePublicId = value;
   }

   public void setDoctypeSystemId(String value)
   {
      doctypeSystemId = value;
   }
   
   public void setPreferDefault(boolean flag) {
      preferDefault = flag;
   }
   
   public void setOmitXMLDeclaration(boolean flag) {
      omitXMLDeclaration = flag;
   }
   
   public void setSkipAncestorNamespaceDeclarations(boolean flag)
   {
      skipAncestorNamespaceDecls = flag;
   }
   
   public void start()
      throws XMLException
   {
      currentBaseURI = null;
      serializer.start();
   }

   public void end()
      throws XMLException
   {
      serializer.end();
   }

   public void setUseCharacterReferences(boolean flag) {
      serializer.setUseCharacterReferences(flag);
   }
   /*
   public void finalize() {
      if (closeOnEnd) {
         try {
            output.close();
         } catch(IOException ex) {

         }
      }
   }

   public void flush()
      throws java.io.IOException
   {
      output.flush();
   }*/
   
   private String generateDecl(boolean isAttribute,String startingPrefix,URI nsName,NamespaceScope originalScope) {
      String prefix = startingPrefix;
      int i = -1;
      if (prefix==null) {
         prefix = originalScope.getNearestPrefix(nsName);
      }
      if (isAttribute && (prefix==Name.NO_PREFIX || (prefix!=null && prefix.equals(Name.NO_PREFIX)))) {
         prefix = null;
      }
      if (prefix==null) {
         i++;
         prefix = "n"+i;
      }
      // If we are at the start, we can bind the default namespace
      if (level==1 && (prefix==Name.NO_PREFIX || prefix.equals(Name.NO_PREFIX)) && scopes[level].getNamespace(prefix)==Name.NO_NAMESPACE) {
         if (log.isLoggable(Level.FINER)) {
            log.finer("Serialization: Generated prefix "+prefix+" -> "+nsName);
         }
         scopes[level].getDataProxy().put(prefix,nsName);
         return prefix;
      }
      boolean prefixFound = false;
      do {
         if (scopes[level].getNamespace(prefix)!=null) {
            i++;
            prefix = "n"+i;
         } else {
            if (log.isLoggable(Level.FINER)) {
               log.finer("Serialization: Generated prefix "+prefix+" -> "+nsName);
            }
            Map<String,URI> proxy = scopes[level].getDataProxy();
            proxy.put(prefix,nsName);
            prefixFound = true;
         }
      } while (!prefixFound);
      return prefix;
      
   }
   
   public Map<String,URI> duplicateProxy(Map<String,URI> proxy) {
      Map<String,URI> newProxy = new HashMap<String,URI>();
      newProxy.putAll(proxy);
      return newProxy;
   }
   
   
   public void write(Item item) 
      throws XMLException
   {
      switch (item.getType()) {
         case DocumentItem: {
            if (serializationStarted) {
               if (level==0) {
                  // catch error where startDocument() is called from sax processors more than once.
                  return;
               }
               throw new XMLException("Document serialization has already been started and document start event was received.");
            }
            level = 0;
            log.finer("Serialization: document start");
            serializer.startDocument();
            serializationStarted = true;
            // TODO: need standalone & encoding
            if (!omitXMLDeclaration) {
               serializer.xmlDeclaration("1.0", null,null);
            }
            currentBaseURI = ((Parent)item).getBaseURI();
            if (!isEvents) {
               Parent doc = (Parent)item;
               if (!doc.isEmpty()) {
                  for (Child c : doc) {
                     write(c);
                  }
               }
               serializer.endDocument();
            }
            scopes[level] = NamespaceScope.getDefaultScope();
         }
         break;
         case DocumentEndItem:
            log.finer("Serialization: document end");
            serializer.endDocument();
         break;
         case ElementItem: {
            boolean elementStartedIt = false;
            if (!serializationStarted) {
               level = 0;
               log.finer("Serialization: document start");
               serializer.startDocument();
               serializationStarted = true;
               elementStartedIt = true;
            }
            boolean finerLog = log.isLoggable(Level.FINER);
            boolean hasExcludedNamespaces = excludedNamespaces.size()!=0;
            if (finerLog) {
               log.finer("Serialization: element start -> "+((Element)item).getName());
            }
            level++;
            
            if (level==scopes.length) {
               NamespaceScope [] newscopes = new NamespaceScope[scopes.length+ARRAY_INCREMENT];
               System.arraycopy(scopes,0,newscopes,0,scopes.length);
               scopes = newscopes;
            }
            Element e = (Element)item;
            String prefix = null;
            Name name = e.getName();
            URI nsName = name.getNamespaceName();
            
            if (level==1) {
               if (elementStartedIt) {
                  scopes[0] = NamespaceScope.getDefaultScope();
               } else {
                  NamespaceScope parentScope = e.getNamespaceScope().getParent();
                  scopes[0] = new NamespaceScope(null, parentScope==null ? NamespaceScope.getDefaultPrefixes() : duplicateProxy(parentScope.getDataProxy()));
               }
            }
            
            scopes[level] = e.hasNamespaceDeclarations() ? new NamespaceScope(scopes[level-1], duplicateProxy(e.getNamespaceScope().getDataProxy())) :
                                                           new NamespaceScope(scopes[level-1], new HashMap<String,URI>());
            Map<String,URI> excludedOverlap = null;
            
            try {
               prefix = scopes[level].getPrefix(name);
               if (finerLog) {
                  log.finer("Prefix is "+prefix);
               }
               if (preferDefault) {
                  URI defaultNS = scopes[level].getNamespace(Name.NO_PREFIX);
                  if (defaultNS.equals(nsName)) {
                     prefix = Name.NO_PREFIX;
                  }
               }
            } catch (NamespaceScope.NotDeclared ex) {
               if (finerLog) {
                  log.finer("Namespace for "+name+" is not declared.  Adding to extras.");
               }
               if (prefix==null) {
                  if (nsName==Name.NO_NAMESPACE) {
                     // If default namespace is declared and no-namespace, the we don't need a prefix.
                     if (scopes[level].getNearestPrefix(Name.NO_NAMESPACE)!=null) {
                        if (finerLog) {
                           log.finer("No need for prefix when there is no default and no prefix.");
                        }
                        prefix = null;
                     }
                     // But if the last scope has default, we need to undeclare it.
                     URI noPrefixNamespace = scopes[level].getNamespace(Name.NO_PREFIX);
                     if (noPrefixNamespace==null || !noPrefixNamespace.equals(Name.NO_NAMESPACE)) {
                        if (finerLog) {
                           log.finer("But the lastScope isn't this scope's parent, so we need to default the namespace.");
                        }
                        scopes[level].getDataProxy().put(Name.NO_PREFIX,Name.NO_NAMESPACE);
                        prefix = Name.NO_PREFIX;
                     }
                  } else {
                     prefix = generateDecl(false,e.getPrefix(),nsName,e.getNamespaceScope());
                  }
               } else {
                  if (finerLog) {
                     log.finer("Re-using parent's prefix: "+prefix);
                  }
               }
            }
            if (hasExcludedNamespaces && excludedNamespaces.get(nsName)!=null) {
               if (excludedOverlap==null) {
                  excludedOverlap = new HashMap<String,URI>();
               }
               excludedOverlap.put(prefix,nsName);
            }
            serializer.startElement(prefix,name);
            URI newBase = e.getBaseURI();
            if (newBase!=null && !newBase.equals(currentBaseURI) && e.getAttributeValue(Attribute.XML_BASE)==null) {
               // Generate an xml:base attribute because the base URI changed and there isn't one specified
               serializer.attribute("xml",Attribute.XML_BASE,newBase.toString());
            }
            currentBaseURI = e.getBaseURI();
            if (e.hasAttributes()) {
                if (finerLog) {
                   log.finer("Processing attributes:");
                }
                Map<Name,Attribute> atts = e.getAttributes();
                for (Name aname : e.getAttributes().keySet()) {
                   Attribute att = atts.get(aname);
                   String aprefix = null;
                   URI attnsName = aname.getNamespaceName();
                   try {
                      aprefix = scopes[level].getPrefix(aname);
                   } catch (NamespaceScope.NotDeclared ex) {
                     if (finerLog) {
                        log.finer("Namespace for attribute name "+aname+" is not declared.");
                     }
                     if (aprefix==null) {
                        if (attnsName==Name.NO_NAMESPACE) {
                        /*
                           if (scopes[level].getNearestPrefix(Name.NO_NAMESPACE)!=null) {
                              aprefix = null;
                           }
                         */
                           aprefix = Name.NO_PREFIX;
                        } else {
                           //System.out.println("here: "+attnsName.equals(Name.NO_NAMESPACE));
                           if (finerLog) {
                              log.finer("Generating prefix map with "+att.getPrefix()+" -> "+attnsName);
                           }
                           aprefix = generateDecl(true,att.getPrefix(),attnsName,e.getNamespaceScope());
                        }
                     }
                   }
                   if ((aprefix==Name.NO_PREFIX || aprefix.equals(Name.NO_PREFIX)) && attnsName!=Name.NO_NAMESPACE) {
                      if (finerLog) {
                         log.finer("Generating prefix map with "+att.getPrefix()+" -> "+attnsName);
                      }
                      aprefix = generateDecl(true,att.getPrefix(),attnsName,e.getNamespaceScope());
                   }
                   serializer.attribute(aprefix, aname,att.getText());
                   if (hasExcludedNamespaces && excludedNamespaces.get(attnsName)!=null) {
                      if (excludedOverlap==null) {
                        excludedOverlap = new HashMap<String,URI>();
                      }
                      excludedOverlap.put(aprefix,attnsName);
                   }
                }
            }


            Map<String,URI> nsProxy = scopes[level].getDataProxy();
            
            if (level==1 && nsProxy.get(prefix)==null) {
               // Not declared on root so the element must be copied from a larger
               // document.  The namespace decl needs to be generated.
               URI ns = scopes[level].getNamespace(prefix);
               if (!(prefix.length()==0 && ns==Name.NO_NAMESPACE)) {
                  // Only do this for the non-empty default namespace
                  if (finerLog) {
                     log.finer("Element copied from larger namespace code, adding "+prefix+" -> "+ns);
                  }
                  serializer.namespaceDeclaration(prefix,ns);
               }
            }
            
            for (String p : nsProxy.keySet()) {
               if (!p.startsWith("xml")) {
                  URI namespace = nsProxy.get(p);
                  URI pnamespace = scopes[level].getParent().getNamespace(p);
                  // Don't output dedundant declarations if the parent has the same definition
                  //log.info(level+","+name+" namespace="+namespace+",pnamespace="+pnamespace);
                  if ((skipAncestorNamespaceDecls || level!=1) && (pnamespace==namespace || pnamespace!=null && pnamespace.equals(namespace))) {
                     //log.info("Skipping "+p+" -> "+namespace+" as declaration is already present on parent.");
                     if (finerLog) {
                        log.finer("Skipping "+p+" -> "+namespace+" as declaration is already present on parent.");
                     }
                     continue;
                  }
                  if (p==Name.NO_PREFIX || p.equals(Name.NO_PREFIX)) {
                     // Don't output the default namespace to nothing if there isn't  default definition
                     // but skip if the level is 1 (which means root element).
                     if (pnamespace==null && level!=1 && namespace==Name.NO_NAMESPACE) {
                        if (finerLog) {
                           log.finer("Skipping "+p+" -> "+namespace+" as there isn't a default declaration yet.");
                        }
                        continue;
                     }
                     // Don't output a default namespace of no namespace at the root
                     if (namespace==Name.NO_NAMESPACE && level==1) {
                        if (finerLog) {
                           log.finer("Skipping "+p+" -> "+namespace+" as this is the root element.");
                        }
                        continue;
                     }
                     if (hasExcludedNamespaces  // there are exclusions 
                         && excludedNamespaces.get(namespace)!=null // it is excluded
                         && (excludedOverlap==null || (excludedOverlap!=null && !excludedOverlap.values().contains(namespace))) // and it isn't in the overlap (required ns) 
                        ) {
                        if (finerLog) {
                           log.finer("Excluded namespace mapping "+p+" -> "+namespace);
                        }
                        // Since we've copied the data proxy, we'll just remove the excluded namespace
                        scopes[level].getDataProxy().remove(p);
                        continue;
                     }
                     if (excludedOverlap!=null) {
                        excludedOverlap.remove(p);
                     }
                  
                     if (finerLog) {
                        log.finer("Added element-level prefix decl "+p+" -> "+namespace);
                     }
                     serializer.namespaceDeclaration(p, namespace);
                  } else {
//                     log.finer("Checking "+namespace+" for exclusion");
//                     log.finer("hasExlcudedNamespaces="+hasExcludedNamespaces);
//                     log.finer("get="+excludedNamespaces.get(namespace));
                     if (hasExcludedNamespaces  // there are exclusions 
                         && excludedNamespaces.get(namespace)!=null // it is excluded
                         && (excludedOverlap==null || (excludedOverlap!=null && !excludedOverlap.values().contains(namespace))) // and it isn't in the overlap (required ns) 
                        ) {
                        // Since we've copied the data proxy, we'll just remove the excluded namespace
                        scopes[level].getDataProxy().remove(p);
                        if (finerLog) {
                           log.finer("Excluded namespace mapping "+p+" -> "+namespace);
                        }
                        continue;
                     }
                     if (excludedOverlap!=null) {
                        excludedOverlap.remove(p);
                     }
                     //log.info(level+" Added element-level prefix decl "+p+" -> "+namespace);
                     if (finerLog) {
                        log.finer("Added element-level prefix decl "+p+" -> "+namespace);
                     }
                     serializer.namespaceDeclaration(p, namespace);
                  }
               }
            }
            
            if (excludedOverlap!=null) {
               for (String overlapPrefix : excludedOverlap.keySet()) {
                  URI ns = excludedOverlap.get(overlapPrefix);
                  if (finerLog) {
                     log.finer("Need to map prefix "+overlapPrefix+" "+ns);
                  }
                  //if (scopes[level-1].getDataProxy().isPrefixExcluded(overlapPrefix)) {
                     serializer.namespaceDeclaration(overlapPrefix, ns);
                     // The prefix is needed here, so put it back
                     scopes[level].getDataProxy().put(overlapPrefix,ns);
                  //}
               }
            }
            
            serializer.endStartElement(name);
            
            // It is possible that we've gotten here and the namespace declarations are not local
            // and at the same time, this is the root element of the document.  This happens when
            // elements are transported.
            // If not events, we need to check for children and handle end tag
            if (!isEvents) {

               // If has children, close and write children
               if (!e.isEmpty()) {
                  for (Child c : e) {
                     write(c);
                  }
               }
               serializer.endElement(prefix, name);
               level--;
            }
            if (level==scopes.length) {
               NamespaceScope [] newscopes = new NamespaceScope[scopes.length+ARRAY_INCREMENT];
               System.arraycopy(scopes,0, newscopes,0, scopes.length);
            }
         }
         break;
         case ElementEndItem: {
            if (log.isLoggable(Level.FINER)) {
               log.finer("Serialization: element end -> "+((ElementEnd)item).getName());
            }
            ElementEnd e = (ElementEnd)item;
            String prefix = null;
            Name name = e.getName();
            NamespaceScope scope = e.getNamespaceScope();
            try {
               prefix = scopes[level].getPrefix(name);
               if (preferDefault) {
                  URI defaultNS = scopes[level].getNamespace(Name.NO_PREFIX);
                  if (defaultNS.equals(name.getNamespaceName())) {
                     prefix = Name.NO_PREFIX;
                  }
               }
            } catch (NamespaceScope.NotDeclared ex) {
               URI nsName = name.getNamespaceName();
               if (nsName==Name.NO_NAMESPACE) {
                  prefix = Name.NO_PREFIX;
               }
            }
            if (prefix==null) {
               throw new XMLException("Fatal error: Null prefix for "+name+" on element-end should not happen.");
            }
            serializer.endElement(prefix,name);
            level--;
         }
         break;
         case CharactersItem:
            log.finer("Serialization: characters");
            {
               Characters c = (Characters)item;
               serializer.characters(c.getText(),c.isCDataSection());
            }
         break;
         case ProcessingInstructionItem:
            log.finer("Serialization: processing instruction");
            {
               ProcessingInstruction pi = (ProcessingInstruction)item;
               serializer.processingInstruction(pi.getName(),pi.getText());
            }
         break;
         case CommentItem:
            log.finer("Serialization: comment");
            {
               Comment c = (Comment)item;
               serializer.comment(c.getText());
            }
      }
   }

   public static void writeDocument(Document doc,Writer out) 
      throws java.io.IOException,XMLException
   {
      XMLWriter writer = new XMLWriter(false,new WriterSerializer(out,"UTF-8",false));
      writer.write(doc);
   }
   
   public static void writeDocument(Document doc,OutputStream os) 
      throws java.io.IOException,XMLException
   {
      writeDocument(doc,"UTF-8",os);
   }
   
   public static void writeDocument(Document doc,String encoding,Writer out) 
      throws java.io.IOException,XMLException
   {
      XMLWriter writer = new XMLWriter(false,new WriterSerializer(out,encoding,false));
      writer.write(doc);
   }
   
   public static void writeDocument(Document doc,String encoding,OutputStream os) 
      throws java.io.IOException,XMLException
   {
      Writer w = new OutputStreamWriter(os,encoding);
      XMLWriter writer = new XMLWriter(false,new WriterSerializer(w,"UTF-8",false));
      writer.write(doc);
      w.flush();
   }
   
   public static void writeElement(Element parent,Writer out)
      throws java.io.IOException,XMLException
   {
      XMLWriter writer = new XMLWriter(false,new WriterSerializer(out,"UTF-8",false));
      writer.write(parent);
   }
   
}
